﻿using System;
using System.IO;
using System.Text.RegularExpressions;

using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;

using SharpSvn;
using System.Text;

namespace SvnTools.Tasks.VersionManagement
{
	public class UpdateVersion : Task
	{
		Regex m_afv  = new Regex(@"^\s*\[assembly:\s*AssemblyFileVersion", RegexOptions.Compiled);
		Regex m_av   = new Regex(@"^\s*\[assembly:\s*(?<attr>AssemblyVersion)\(""(?<major>\d+)\.(?<minor>\d+)(\.\*|\.(?<patch>\d+)|)(\.\*|\.(?<rev>\d+)|)""\)\]", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
		Regex m_pv   = new Regex("^\\s*\"ProductVersion\" = \"8:\\d+\\.\\d+\\.\\d+(\\.\\d+)?\"");
		Regex m_pc   = new Regex("^\\s*\"ProductCode\" = \"8:(\\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\\}{0,1})\"");
		Regex m_pkgc = new Regex("^\\s*\"PackageCode\" = \"8:(\\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\\}{0,1})\"");
		Regex m_uc   = new Regex("^\\s*\"UpgradeCode\" = \"8:(\\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\\}{0,1})\"");

		public ITaskItem[] AssemblyInfoFiles { get; set; }

		public ITaskItem Format { get; set; }

		public ITaskItem[] DeploymentProjectFiles { get; set; }

		public ITaskItem DeployFormat { get; set; }

		public ITaskItem[] ProductCodes { get; set; }

		public ITaskItem[] PackageCodes { get; set; }

		public ITaskItem[] UpgradeCodes { get; set; }

		public ITaskItem SvnRevisionQueue { get; set; }

		public override bool Execute()
		{
			var  proj_dir  = Path.GetDirectoryName(BuildEngine.ProjectFileOfTaskNode);
			var  proj_path = string.Empty;
			long revnum    = 0;
			int  build_num = 0;
			if (Format == null || Format.ItemSpec == string.Empty)
				Format = new TaskItem("major.minor.rev_m.rev_l");

			if (DeployFormat == null || DeployFormat.ItemSpec == string.Empty)
				DeployFormat = new TaskItem("major.rev_m.rev_l");

			if (SvnRevisionQueue == null || SvnRevisionQueue.ItemSpec == string.Empty)
				SvnRevisionQueue = new TaskItem("LastChanged");

			using( var client = new SvnClient() )
			{
				if( IsClean(proj_dir, client) )
				{
					revnum = GetRevision(proj_dir, client);

					if( revnum > 0 )
					{
						string repo_root = string.Empty;
						string proj_uri  = string.Empty;

						client.Info(SvnTarget.FromString(proj_dir), (s, e) =>
							{
								repo_root = e.RepositoryRoot.ToString();
								proj_uri  = e.Uri.ToString();
							});

						if( !proj_uri.StartsWith(repo_root) )
							Log.LogError("Project URI did NOT contain the repository root URI");

						proj_path = proj_uri.Remove(0, repo_root.Length);
					}
				}
			}

			// TODO: get the build number for our revision
			if( !string.IsNullOrEmpty(proj_path) )
				build_num = 0;

			if (AssemblyInfoFiles != null)
			{
				foreach (var item in AssemblyInfoFiles)
				{
					if (UpdateFile(item.ItemSpec, revnum, build_num, false, 0))
						Log.LogMessage(MessageImportance.Normal, "Successfully updated file.");
					else
						Log.LogWarning("Version attribute not found, file not updated.");
				}
			}

			if (DeploymentProjectFiles != null)
			{
				for (int i = 0; i < DeploymentProjectFiles.Length; i++)
				{
					var item = DeploymentProjectFiles[i];
					if (UpdateFile(item.ItemSpec, revnum, build_num, true, i))
						Log.LogMessage(MessageImportance.Normal, "Successfully updated file.");
					else
						Log.LogMessage("ProductVersion not found, file not updated.");
				}
			}

			return true;
		}

		public bool UpdateFile(string filename, long rev_num, int build_num, bool isDeploy, int index)
		{
			bool   found  = false;
			string fn_org = filename + ".vnbak";

			// if we have an unreverted change, revert it first
			if( File.Exists(fn_org) )
				RevertVersionChange.RevertFile(filename, Log);

			// then, if we have no file, bail out
			if( !File.Exists(filename) )
			{
				Log.LogMessage(MessageImportance.High, "Version information file not found: {0}", filename);
				return false;
			}

			Log.LogMessage(MessageImportance.Normal, "Updating version number (to rev {1}) for file: {0}", filename, rev_num);

			File.Move(filename, fn_org);

			using( var sr = new StreamReader(fn_org) )
			using( var sw = new StreamWriter(filename, false, sr.CurrentEncoding) )
			{
				while( sr.Peek() > -1 )
				{
					var line = sr.ReadLine();

					if (!isDeploy && m_afv.IsMatch(line))
					{
						Log.LogWarning("AssemblyFileVersion attribute found; this attribute should be omitted in favor of the AssemblyVersion attribute.");
						continue;
					}

					bool success = true;
					if (isDeploy)
					{
						Guid packageCode = Guid.NewGuid();
						Guid productCode = packageCode;
						Guid upgradeCode = Guid.Empty;
						if (ProductCodes != null && index < ProductCodes.Length)
							productCode = new Guid(ProductCodes[index].ItemSpec);
						if (PackageCodes != null && index < PackageCodes.Length)
							packageCode = new Guid(PackageCodes[index].ItemSpec);
						if (UpgradeCodes != null && index < UpgradeCodes.Length)
							upgradeCode = new Guid(UpgradeCodes[index].ItemSpec);

						if (m_pv.IsMatch(line))
							BuildVersion(sw, m_pv.Match(line), rev_num, true, line);
						else if (m_pc.IsMatch(line))
							sw.WriteLine(ReplaceDeployValue(line, "ProductCode", "{" + productCode.ToString().ToUpper() + "}"));
						else if (m_pkgc.IsMatch(line))
							sw.WriteLine(ReplaceDeployValue(line, "PackageCode", "{" + packageCode.ToString().ToUpper() + "}"));
						else if (upgradeCode != Guid.Empty && m_uc.IsMatch(line))
							sw.WriteLine(ReplaceDeployValue(line, "UpgradeCode", "{" + upgradeCode.ToString().ToUpper() + "}"));
						else
							success = false;
					}
					else
					{
						if (m_av.IsMatch(line))
							BuildVersion(sw, m_av.Match(line), rev_num, false, line);
						else
							success = false;
					}

					if(!success)
					{
						sw.WriteLine(line);
						continue;
					}
					else
						found = true;
				}

				sw.Close();
				sr.Close();
			}

			return found;
		}

		/// <summary>
		/// Builds the version in either deploy or assembly cases.
		/// </summary>
		private void BuildVersion(StreamWriter sw, Match m, long rev_num, bool isDeploy, string line)
		{
			string major = !isDeploy ? m.Groups["major"].Value : "1";
			string minor = !isDeploy ? m.Groups["minor"].Value : "0";
			string rnum_l = rev_num.ToString();
			string rnum_m = "0";

			if (rnum_l.Length > 4)
			{
				rnum_m = rnum_l.Substring(0, rnum_l.Length - 4);
				rnum_l = rnum_l.Substring(rnum_l.Length - 4);
			}

			if (string.IsNullOrEmpty(major))
				major = "0";

			if (string.IsNullOrEmpty(minor))
				minor = "0";

			if (rev_num == 0)
			{
				if (!isDeploy)
					sw.WriteLine("[assembly: AssemblyVersion(\"0.0.0.0\")]");
				else
					sw.WriteLine(ReplaceDeployValue(line, "ProductVersion", "0.0.0"));
			}
			else
			{
				DateTime now = DateTime.Now;
				string format = isDeploy ? DeployFormat.ItemSpec : Format.ItemSpec;
				string[] orders = format.Split('.');

				StringBuilder builder = new StringBuilder();
				for (int i = 0; i < orders.Length; i++)
				{
					switch (orders[i].ToLower())
					{
						case "yyyy":
							builder.Append(now.Year);
							break;
						case "mm":
							builder.Append(now.Month);
							break;
						case "dd":
							builder.Append(now.Day);
							break;
						case "rev":
							builder.Append(rev_num);
							break;
						case "rev_l":
							builder.Append(rnum_l);
							break;
						case "rev_m":
							builder.Append(rnum_m);
							break;
						case "major":
							builder.Append(major);
							break;
						case "minor":
							builder.Append(minor);
							break;
						default:
							builder.Append(orders[i]);
							break;
					}
					if (i < orders.Length - 1)
						builder.Append(".");
				}
				if (!isDeploy)
					sw.WriteLine("[assembly: AssemblyVersion(\"{0}\")]", builder.ToString());
				else
					sw.WriteLine(ReplaceDeployValue(line, "ProductVersion", builder.ToString()));
			}
		}

		/// <summary>
		/// In a line of the format 
		/// name = "8:value"
		/// will set nameed value to a new value
		/// </summary>
		/// <param name="valueLine">The line being translated</param>
		/// <param name="name">The name of the pair</param>
		/// <param name="newValue">The new value</param>
		/// <returns>The translated line</returns>
		protected string ReplaceDeployValue(string valueLine, string name, string newValue)
		{
			// find the position of the equal sign
			int eqPos = valueLine.IndexOf('=');
			if (eqPos == -1)
				throw new Exception("File format does not conform to the expected layout");

			// then splice the part before the equal sign with the new value 
			// (including the '8:' that the vdproj format uses for these properties)
			string newString = valueLine.Substring(0, eqPos + 1);
			newString += string.Format(" \"8:{0}\"", newValue);

			return newString;
		}

		private bool IsClean(string dir, SvnClient client)
		{
			var args = new SvnStatusArgs()
			{
				Depth                  = SvnDepth.Infinity,
				IgnoreExternals        = true,
				RetrieveAllEntries     = true,
				RetrieveIgnoredEntries = true,
			};

			var clean = true;

			client.Status(dir, args, (s, e) =>
				{
					// if it's switched, definitely not clean.
					if( e.Switched )
					{
						Log.LogWarning("Working copy not clean; switched directory found: {0}", e.FullPath);

						clean = false;
						return;
					}

					// if it's not versioned and not allowed to be ignored, not clean.
					if( e.LocalContentStatus == SvnStatus.NotVersioned || e.LocalContentStatus == SvnStatus.Ignored )
					{
						if( IsIgnored(e.FullPath) )
							return;

						Log.LogWarning("Working copy not clean; disallowed artifact found: {0}", e.FullPath);

						clean = false;
						return;
					}

					// if it's content status isn't normal or external, it's not clean
					if( e.LocalContentStatus != SvnStatus.Normal && e.LocalContentStatus != SvnStatus.External )
					{
						Log.LogWarning("Working copy not clean; {1} artifact found: {0}", e.FullPath, e.LocalContentStatus);

						clean = false;
						return;
					}

					// if it's property status isn't normal or external, it's not clean
					if( e.LocalPropertyStatus != SvnStatus.Normal && e.LocalPropertyStatus != SvnStatus.None )
					{
						Log.LogWarning("Working copy not clean; {1} property found: {0}", e.FullPath, e.LocalPropertyStatus);

						clean = false;
						return;
					}
				});

			return clean;
		}

		private long GetRevision(string dir, SvnClient client)
		{
			var args = new SvnInfoArgs()
			{
				Depth = SvnDepth.Infinity,
			};

			long rev  = -1;
			long crev = 0;

			client.Info(SvnTarget.FromString(dir), args, (s, e) =>
				{
					if( rev == -1 )
						rev = e.Revision;
					else if( rev > 0 && rev != e.Revision )
						rev = 0;

					if( e.LastChangeRevision > crev )
						crev = e.LastChangeRevision;
				});

			if (rev > 0)
				return SvnRevisionQueue.ItemSpec.Equals("LastChanged") ? crev : rev;
			else
				return 0;
		}

		private bool IsIgnored(string p)
		{
			if( Directory.Exists(p) )
			{
				var dname = Path.GetFileName(p).ToLower();

				if( dname == "bin" || dname == "obj" || dname == "TestResults" || dname == "debug" || dname == "release" || dname == "artifacts")
					return true;
			}

			if( File.Exists(p) )
			{
				var fname = Path.GetFileName(p);

				if( fname.EndsWith(".csproj.user") || fname.EndsWith(".sln.cache") || fname.EndsWith(".suo") || fname.EndsWith(".vnbak") || fname == "StyleCop.Cache" )
					return true;
			}

			return false;
		}

	}
}
